Multi-page studio: every section a real indexable URL (View Transitions; docker-compose in the menu)#6
Conversation
…compose in Explorer) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…N-LD Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oy.astro & docker-compose-example.astro Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…les survive View Transitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR converts the “studio” experience from an in-page hash-swapped SPA into a multi-page, SEO-indexable site where each Explorer section is its own static URL, with Astro View Transitions providing SPA-like navigation while keeping the IDE chrome persistent.
Changes:
- Adds a dynamic section route (
src/pages/[section].astro) backed by section slugs/SEO metadata and per-section JSON-LD injection. - Updates studio navigation + interactions to be URL-driven (
studio.tssyncs active state from pathname; hash swap engine removed) and enables View Transitions via<ClientRouter/>+transition:persist. - Introduces the Docker Compose page as a first-class studio section (
docker_compose) with single-source content and structured data.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/styles/global.css | Removes desktop “active section swap” CSS; single section always fills the results pane. |
| src/scripts/studio.ts | Replaces hash-routing with URL-based state; adds View-Transition lifecycle wiring and delegated handlers. |
| src/pages/index.astro | Makes / render home-only using section SEO fields. |
| src/pages/docker-compose-example.astro | Deletes the old standalone page (replaced by dynamic section routing). |
| src/pages/deploy.astro | Deletes the old standalone page (replaced by dynamic section routing). |
| src/pages/[section].astro | New dynamic route to generate each section page + inject per-section JSON-LD. |
| src/layouts/Layout.astro | Adds Astro View Transitions <ClientRouter /> in <head>. |
| src/data/sections.ts | Adds slug/pageTitle/pageDescription to section metadata and adds docker_compose. |
| src/data/sections.test.ts | Adds tests for slug uniqueness and presence of per-section SEO fields. |
| src/data/section-seo.ts | Adds per-section JSON-LD map for deploy + docker-compose. |
| src/components/studio/StudioShell.astro | Persists chrome elements with transition:persist. |
| src/components/studio/MobileTopBar.astro | Updates drawer Explorer rendering consistent with URL navigation. |
| src/components/studio/Explorer.astro | Switches section links from #hash to real URLs derived from slugs. |
| src/components/sections/DockerComposeSection.astro | New single-source docker-compose section content (lifted from the old page). |
| src/components/Header.astro | Updates top nav links from /#... to /.... |
| src/components/Footer.astro | Updates footer links from /#... to /.... |
| package.json | Bumps version to 0.4.2. |
| docs/superpowers/plans/2026-06-23-multipage-studio.md | Adds implementation plan documentation for the multipage studio work. |
| docs/designs/2026-06-23-multipage-studio-design.md | Adds design documentation for routing/SEO/navigation decisions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function onPage() { | ||
| syncActive(); | ||
| } |
There was a problem hiding this comment.
Good catch — confirmed at runtime (after a VT nav the new .studio lost .js and the desktop viewport-lock broke). Fixed in 47e65e3: the .js class is now re-applied to the current [data-studio] on every astro:page-load (per-page path), not only once. Verified: after navigating, .studio has .js and overflow is hidden again.
| @@ -8,7 +8,6 @@ interface Props { | |||
| standalone?: boolean; | |||
There was a problem hiding this comment.
Fixed in 47e65e3 — removed the unused standalone prop from Explorer's interface + destructuring.
| const { id } = Astro.props as { id: string }; | ||
| const meta = sectionById[id]; | ||
| const Body = COMPONENTS[id]; | ||
| const jsonLd = sectionSeo[id] ?? []; |
There was a problem hiding this comment.
Fixed in 47e65e3 — added if (!Body) throw new Error([section].astro: no component mapped for section id "${id}") right after resolving Body, so a missing COMPONENTS mapping fails with a clear message.
| function onActionClick(e: Event) { | ||
| const target = e.target as HTMLElement; | ||
|
|
There was a problem hiding this comment.
Fixed in 47e65e3 — onActionClick now guards const target = e.target; if (!(target instanceof Element)) return; before any .closest().
| document.addEventListener('keydown', (e) => { | ||
| if (e.key !== 'Enter') return; | ||
| const searchInput = (e.target as HTMLElement).closest<HTMLInputElement>('[data-explorer-search]'); | ||
| if (!searchInput) return; |
There was a problem hiding this comment.
Fixed in 47e65e3 — the delegated keydown (explorer-search Enter-to-jump) now guards e.target instanceof Element and uses a local target.
| </div> | ||
| <div class="h-[calc(100%-49px)]"> | ||
| <Explorer active={active} idPrefix="drawer" showConnectionsLabel={false} standalone={standalone} /> | ||
| <Explorer active={active} idPrefix="drawer" showConnectionsLabel={false} /> |
There was a problem hiding this comment.
Fixed in 47e65e3 — removed the unused standalone from MobileTopBar's Props + destructuring (and stopped forwarding it to Explorer).
…ts; clearer [section] error; drop unused standalone prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // Chrome delegations: palette-close, drawer open/close, explorer column toggles | ||
| if (target.closest('[data-palette-close]')) { e.preventDefault(); closePalette(); return; } | ||
| if (target.closest('[data-drawer-open]')) { e.preventDefault(); openDrawer(); return; } | ||
| if (target.closest('[data-drawer-close]')) { e.preventDefault(); closeDrawer(); return; } | ||
| const toggleBtn = target.closest<HTMLElement>('[data-explorer-toggle]'); |
There was a problem hiding this comment.
Good catch — fixed in 116748d. Since section links are plain anchors now, I added an astro:before-swap listener in studio.ts that force-closes the persisted drawer (re-adds -translate-x-full / hidden, sets aria-expanded=false) and resets document.body.style.overflow before each swap. Runtime-verified on the production build at mobile width: opening the drawer and clicking a section link inside it now navigates with the drawer hidden and body.overflow reset to "".
…buttons, stars) after transitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/components/studio/StudioShell.astro:44
- The Studio interaction bootstrap is loaded via an inline
<script>that imports../../scripts/studio.ts. When navigating into the Studio from a non-studio page using View Transitions (e.g.,/privacy-policy→/features), this script may not execute, leaving chrome interactions unbound. Mark the import script withdata-astro-rerunso it reliably runs after client-side navigations (the module itself is guarded withwiredOnce).
<script>
import '../../scripts/studio.ts';
</script>
| <title>{title}</title> | ||
| <ClientRouter /> | ||
| <slot name="head" /> |
There was a problem hiding this comment.
Valid — global <ClientRouter/> did break these on client-side navigation. Fixed across two commits (27fbb49, 7c3c96e):
- CookieConsent (
is:inline): markeddata-astro-rerunso the inline script re-executes on every transition — it rebinds accept/decline on the freshly-swapped DOM and now records a GApage_viewevent + MetaPageViewper SPA navigation (already-loaded state kept onwindow). I first tried anastro:page-loadlistener inside the inline script, but it did not fire after a transition;data-astro-rerunis the reliable pattern for inline scripts. - Footer accordion: added
data-astro-rerunto its<script>so the listeners rebind on VT arrivals at/privacy-policy&/404.
Runtime-verified on the production build (with caching disabled): banner re-shows + Accept works after a VT nav; GA/Meta fire on accept and a fresh page_view fires on each subsequent SPA navigation; the footer accordion toggles after a privacy→home→privacy round-trip.
…across View Transitions
…-astro-rerun The astro:page-load listener approach (27fbb49) did not fire for the is:inline consent script after a client-side navigation. Switch to data-astro-rerun so the inline script itself re-executes on every transition: it rebinds the accept/decline buttons on the freshly-swapped DOM, records a GA/Meta page_view per SPA navigation, and keeps already-loaded state on window (which persists across swaps). All analytics globals are window-qualified. Runtime-verified on the production build with caching disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-page studio — every section is its own indexable URL
Generalizes the
/deploypattern from #5 to the whole site: each Explorer "table" is now a real, indexable page navigated via Astro View Transitions — the in-page#hashswap is gone. Same IDE shell and interactions; smoother, SPA-like, and far better for SEO. Per the user's request,/docker-compose-exampleis now the 9th Explorer table (in the left menu).Spec:
docs/designs/2026-06-23-multipage-studio-design.md· Plan:docs/superpowers/plans/2026-06-23-multipage-studio.mdWhat changed
src/pages/[section].astro(getStaticPathsover slugs) generates/features,/databases,/compare,/tech-stack,/get-started,/faq,/deploy,/docker-compose-example;index.astro=/home-only.deploy.astro+docker-compose-example.astrodeleted (the route + section components supersede them).slug/pageTitle/pageDescription; a 9thdocker_composetable added.section-seo.tsholds per-section JSON-LD (deploy ItemList; docker-compose HowTo + SourceCode), injected into<head>via a Layoutheadslot.DockerComposeSection.astro: single-source compose content (quick-start, full yml, env tables, copy) lifted from the old page.studio.ts: swap/hash engine removed; navigation by URL;currentId()from pathname;syncActive()updates the active row + status bar on everyastro:page-load.<ClientRouter/>+transition:persiston the chrome (topbar, sidebar, statusbar, mobile bar, console, palette); the result pane transitions.transition:persistdid not preserve DOM node identity, so once-bound chrome listeners went stale after navigation — all chrome interactions are now document-level delegated, so palette open/close, search, column toggles, drawer, and action buttons survive every navigation (runtime-verified)./#sectionlinks → real paths./privacy-policy+404left standalone (out of scope).Verification
syncActiveper nav, and every chrome interaction working after navigation (incl. mobile drawer + direct-load of section URLs).bunx astro build→ 11 pages.bun test→ 24/24. Single-source proven. 0href="/#"links. SEO: 2 → 10 indexable URLs, deploy/docker-compose URLs + structured data preserved.🤖 Generated with Claude Code